package ru.papergames.battleserver.model.battle;

import javolution.util.FastMap;
import org.apache.log4j.Logger;
import ru.common.util.MTRandom;
import ru.papergames.Config;
import ru.papergames.battleserver.model.battle.services.*;
import ru.papergames.battleserver.model.battle.map.BattleMap;
import ru.papergames.battleserver.model.battle.statisticscalculators.DefaultStatisticsCalculator;
import ru.papergames.battleserver.network.WebServerClient;
import ru.papergames.battleserver.network.serverpackets.flashuser.*;
import ru.papergames.battleserver.network.serverpackets.webserver.BattleFinishNotification;
import ru.papergames.battleserver.network.serverpackets.webserver.BattleStartNotification;
import ru.papergames.common.enums.BattleType;

import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * TODO: replace all RuntimeExceptions with proper handling
 *
 * @author: atroshanin
 * Date: 09.04.2010
 * Time: 22:52:55
 */
public abstract class AbstractBattle {

    private static final Logger log = Logger.getLogger(AbstractBattle.class.getName());

    private final int battleOrderId;
    private WebServerClient wsClient;

    protected ArrayList<BattlePlayer> battleMembers;
    protected FastMap<Long, BattlePlayer> battleMembersMap;
    protected FastMap<Long, BattlePlayer> battlePcMembersMap;
    protected FastMap<Long, BattlePlayer> battleNpcMembersMap;
    private FastMap<Long, BattlePlayer> disconnectedBattlePcMembersMap;
    protected BattleMap battleMap;
    private int teamsCount;
    protected BattleType battleType;
    private ArrayList<ArrayList<BattlePlayer>> teams;

    /**
     * This lock is used to do any action. Because during action the time of turn might end
     * and TurnController can change turn and playerMovingNow, so we have to prevent it.
     * So TurnControllerThread might not change turn if some action is executed.
     * Also action will not be executed if TurnController already changed turn and playerMovingNow
     */
    protected Lock actionLock = new ReentrantLock();

    /**
     * this lock is for locking parallel access to actions from soldiers of one BattlePlayer
     * Because two different players can not access battle actions(move, attack, etc)
     */
//    private Lock syncActionLock = new ReentrantLock();

    private Random rand = MTRandom.getInstance().getSecureRandom();

    protected boolean winnerFound;
    protected int winnerTeamId = -2;

    protected BattleState battleState = BattleState.INIT;
    protected long battleStateClientSyncStartTime;

    protected int ticksBeforeStart = Config.TICKS_BEFORE_BATTLE_START;

    protected long creationTime;
    protected boolean fakeBattle;

    private int playerIdCounter = 1;
    private int soldierIdCounter = 1;

    protected LinesFinder linesFinder;
    protected NewBallsThrower newBallsThrower;

    private BallColor[] colorsSetForRandomAccess = null;

    public AbstractBattle(int battleOrderId, WebServerClient wsClient, int maxPcMembersCount, int maxNpcMembersCount, int teamsCount, BattleMap battleMap) {
        this.battleOrderId = battleOrderId;
        this.wsClient = wsClient;
        battleMembers = new ArrayList<BattlePlayer>(maxPcMembersCount + maxNpcMembersCount);
        battleMembersMap = new FastMap<Long, BattlePlayer>(maxPcMembersCount + maxNpcMembersCount).shared();
        battlePcMembersMap = new FastMap<Long, BattlePlayer>(maxPcMembersCount).shared();
        disconnectedBattlePcMembersMap = new FastMap<Long, BattlePlayer>(maxPcMembersCount).shared();
        battleNpcMembersMap = new FastMap<Long, BattlePlayer>(maxNpcMembersCount).shared();
        this.battleMap = battleMap;
        this.teamsCount = teamsCount;
        this.teams = new ArrayList<ArrayList<BattlePlayer>>(teamsCount);
        for (int i = 0; i < teamsCount; i++) {
            this.teams.add(new ArrayList<BattlePlayer>());
        }
        creationTime = System.currentTimeMillis();

        linesFinder = AllLinesFinderImpl.getInstance();
        newBallsThrower = DefaultNewBallsThrowerImpl.getInstance();
    }

    // *********************************************************************
    // ***********************Getters & Setters*****************************
    // *********************************************************************

    public int getBattleOrderId() {
        return battleOrderId;
    }

    public BattleState getBattleState() {
        return battleState;
    }

    public void setBattleState(BattleState battleState) {
        this.battleState = battleState;
    }

    public Lock getActionLock() {
        return actionLock;
    }

    public abstract boolean isPlayerMovingNow(BattlePlayer player);

    public boolean isBattleIniting() {
        return battleState == BattleState.INIT;
    }

    public boolean isBattleRunning() {
        return battleState == BattleState.RUNNING;
    }

    public boolean isBattleFinished() {
        return battleState == BattleState.FINISHED || battleState == BattleState.TO_BE_REMOVED;
    }

    public boolean isBattleToBeRemoved() {
        return battleState == BattleState.TO_BE_REMOVED;
    }

    private boolean addMembersPossible() {
        return battleState == BattleState.INIT || battleType == BattleType.ASSAULT;
    }

    public boolean isInitingTimeout() {
        return !fakeBattle && (System.currentTimeMillis() - creationTime) > Config.BATTLE_INIT_TIMEOUT;
    }

    public BattleMap getBattleMap() {
        return battleMap;
    }

    public FastMap<Long, BattlePlayer> getBattleMembers() {
        return battleMembersMap;
    }

    public Collection<BattlePlayer> getBattlePcMembers() {
        return battlePcMembersMap.values();
    }

    public boolean isWinnerFound() {
        return winnerFound;
    }

    public BattleType getBattleType() {
        return battleType;
    }

    public int getWinnerTeamId() {
        return winnerTeamId;
    }

    public boolean isFakeBattle() {
        return fakeBattle;
    }

    public void setFakeBattle(boolean fakeBattle) {
        this.fakeBattle = fakeBattle;
    }

    public void broadcastToBattleMembers(PaperGameServerPacket msg) {
        for (FastMap.Entry<Long, BattlePlayer> e = battleMembersMap.head(), end = battleMembersMap.tail(); (e = e.getNext()) != end;) {
            BattlePlayer member = e.getValue();
            if (member != null) {
                member.sendPacket(msg);
            }
        }
    }

    public void broadcastToBattleMembers(PaperGameServerPacket msg, BattlePlayer excludeMember) {
        for (FastMap.Entry<Long, BattlePlayer> e = battleMembersMap.head(), end = battleMembersMap.tail(); (e = e.getNext()) != end;) {
            BattlePlayer member = e.getValue();
            if (member != null && member != excludeMember) {
                member.sendPacket(msg);
            }
        }
    }

    // *********************************************************************
    // ****************Methods for initializing/Starting battle*************
    // *********************************************************************

    public void addBattleMember(BattlePlayer member) {
        addBattleMember(member, false);
    }

    public void addBattleMember(BattlePlayer member, boolean join) {
        if (!addMembersPossible()) {
            throw new RuntimeException("impossible to add members in battle");
        }
        if (member.getTeamId() < 0 || member.getTeamId() >= teamsCount) {
            throw new RuntimeException("incorrect team id");
        }
        if (member.isPcPlayer()) {
            BattlePlayer existingPlayer = battlePcMembersMap.get(member.getId());
            if (existingPlayer != null) {
                throw new RuntimeException("Player with such dbId[" + member.getId() + "] already exists");
            }
            battlePcMembersMap.put(member.getId(), member);
        } else {
            BattlePlayer existingPlayer = battleNpcMembersMap.get(member.getId());
            if (existingPlayer != null) {
                throw new RuntimeException("NPCPlayer with such dbId[" + member.getId() + "] already exists");
            }
            battleNpcMembersMap.put(member.getId(), member);
        }
        ArrayList<BattlePlayer> team = teams.get(member.getTeamId());
        synchronized (team) {
            team.add(member);
        }
        battleMembers.add(member);
        battleMembersMap.put(member.getId(), member);
    }

    public void addBattleMembers(ArrayList<BattlePlayer> battleMembers) {
        for (BattlePlayer member : battleMembers) {
            addBattleMember(member);
        }
    }

    public void arrangeTurnsAndSpawnSoldiers() {
        arrangeMemberTurns();
    }

    public void sendBattleInfoToMembers() {
        broadcastToBattleMembers(new BattleMapInfo(this));
        for (FastMap.Entry<Long, BattlePlayer> e = battleMembersMap.head(), end = battleMembersMap.tail(); (e = e.getNext()) != end;) {
            BattlePlayer member = e.getValue();
            member.sendPacket(new BattleMemberInfoAboutHimself(member));
        }
        broadcastToBattleMembers(new BattleMemberList(this));

        battleState = BattleState.CLIENT_SYNCHRONIZATION_AFTER_START;
    }

    /**
     * Sending info about battle player to it's owner.
     * Sending all info about members of battle when member has just connected and going to start battle
     * @param member - battle player that gas just connected.
     */
    public void onBattleMemberConnected(BattlePlayer member) {
        member.sendPacket(new BattleMemberInfoAboutHimself(member));
        member.sendPacket(new BattleMemberList(this));
    }

    public void checkAllClientsLoadBattle() {
        for (FastMap.Entry<Long, BattlePlayer> e = battleMembersMap.head(), end = battleMembersMap.tail(); (e = e.getNext()) != end;) {
            BattlePlayer member = e.getValue();
            if (!member.isBattleLoaded()) {
                if (System.currentTimeMillis() - battleStateClientSyncStartTime > Config.CLIENT_SYNC_TIMEOUT) {
//                    battleState = BattleState.FINISHED;
                    System.out.println("AbstractBattle.checkAllClientsLoadBattle Starting battle by CLIENT_SYNC_TIMEOUT");
                    break;
                }
                return;
            }
        }

        /*
        // adding NPCs to processing
        for (BattlePlayer npcPlayer : battleNpcMembersMap.values()) {
            if (!npcPlayer.isPcPlayer()) {
                BattleSoldier [] npcSoldiers = new BattleSoldier[npcPlayer.getSoldiers().size()];
                npcPlayer.getSoldiers().toArray(npcSoldiers);
                NpcsManager.getInstance().addNpcToProcess(npcSoldiers);
            }
        }
        // adding GAME-NPCs to processing
        for (BattlePlayer pcPlayer : battlePcMembersMap.values()) {
            if (pcPlayer.isPcPlayer() && pcPlayer.getOsidePlayer().isGameNpc()) {
                BattleSoldier [] npcSoldiers = new BattleSoldier[pcPlayer.getSoldiers().size()];
                pcPlayer.getSoldiers().toArray(npcSoldiers);
                NpcsManager.getInstance().addNpcToProcess(npcSoldiers);
            }
        }*/
        System.out.println("[Battle-Init-Finished] initTime[" + (System.currentTimeMillis() - creationTime) + "] pcMembersSize[" + battlePcMembersMap.size() + "]");
        battleState = BattleState.RUNNING;
        winnerTeamId = -1; // nobody winn

        onAllClientsLoaded();
    }

    protected void onAllClientsLoaded() {
        notifyWebServerAboutBattleStart();
        startCurrentTurn();
    }

    public boolean isAllClientsConnected() {
        for (FastMap.Entry<Long, BattlePlayer> e = battleMembersMap.head(), end = battleMembersMap.tail(); (e = e.getNext()) != end;) {
            BattlePlayer member = e.getValue();
            if (!member.isConnected()) {
                return false;
            }
        }
        return true;
    }

    public boolean startAutomatically() {
        return true;
    }

/*
    public void processStartingTask() {
        if (ticksBeforeStart == Config.TICKS_BEFORE_BATTLE_START) {
            broadcastToBattleMembers(new BattleMembersArranged(battleMembersMap)); // info about members and its turns
            broadcastToBattleMembers(new BattleMapInfo(battleMap)); // info about map(size, gorund etc)
            broadcastToBattleMembers(new BattleSoldiersPositions(battleSoldiers)); // info about soldiers and their HP and etc
            broadcastToBattleMembers(BattleSent.STATIC_PACKET); // Finish
        }
        if (ticksBeforeStart % 10 == 0) {
            SystemMessage msg = new SystemMessage(SystemMessageId.BATTLE_STARTING_IN_S1_SECONDS);
            msg.addNumber(ticksBeforeStart/10);
            broadcastToBattleMembers(msg);
        }
        if (ticksBeforeStart <= 0) {
            battleState = BattleState.CLIENT_SYNCHRONIZATION_AFTER_START;
            battleStateClientSyncStartTime = System.currentTimeMillis();
        } else {
            ticksBeforeStart = ticksBeforeStart - 2;
        }
    }
*/

    protected abstract void arrangeMemberTurns();

    // *********************************************************************
    // ******Methods for managing battle(chane turn, finish, etc)***********
    // *********************************************************************

    public abstract boolean isBattleTurnProceeds();

    public abstract void finishPlayerTurn();

    public abstract boolean choseNextPlayerForTurn();

    public abstract void startCurrentTurn();

    public void finishBattle() {
        // calculate statistics(experience)
        DefaultStatisticsCalculator.getInstance().calculateStatistics(battleType, winnerFound, winnerTeamId, teams);

        for (BattlePlayer pcBattleMember : battlePcMembersMap.values()) {
            pcBattleMember.sendPacket(new BattleFinished(pcBattleMember, winnerFound && pcBattleMember.getTeamId() == winnerTeamId));
            if (pcBattleMember.getGameUser() != null) {
                pcBattleMember.getGameUser().setNormalState();
            }
        }
        battleState = BattleState.FINISHED;
    }

    public void onPlayerDisconnect(BattlePlayer battleMember) {
        if (isBattleFinished()) {
            return;
        }
        disconnectedBattlePcMembersMap.put(battleMember.getId(), battleMember);
        broadcastToBattleMembers(new BattlePlayerDisconnected(battleMember.getId()));
    }

    /*
//    public BattlePlayer onPlayerReconnect(int playerDbId, String secretKey) {
    public BattlePlayer onPlayerReconnect(int playerDbId, int secretKeyL) {
        BattlePlayer battleMember = disconnectedBattlePcMembersMap.remove(playerDbId);
//        if (battleMember != null && battleMember.getSecretKey().equals(secretKey)) {
        if (battleMember != null && battleMember.getSecretKeyL() == secretKeyL) {
            broadcastToBattleMembers(new SystemMessage(SystemMessageId.S1_RECONNECTED_TO_BATTLE).addNumber(battleMember.getPlayerId()), battleMember);
            return battleMember;
        }
        return null;
    } */

    public void notifyWebServerAboutBattleFinish() {
        log.info("AbstractBattle.notifyWebServerAboutBattleFinish[" + battleOrderId + "]");
        if (!fakeBattle) {
            //wsClient.sendPacket(new BattleFinishNotification(this));
            wsClient.sendPacket(new BattleFinishNotification(this));
        }
    }

    public void notifyWebServerAboutBattleStart() {
        log.info("AbstractBattle.notifyWebServerAboutBattleStart[" + battleOrderId + "]");
        if (!fakeBattle) {
            wsClient.sendPacket(new BattleStartNotification(this));
        }
    }

    // *********************************************************************
    // **Methods for user actions while battle is running(move, atack, etc)*
    // *********************************************************************

    /**
     * Method for all battle actions that MUST NEVER be called by two threads at same time(move, attack, etc)
     * Now(from 2010.04.27) we determine that all actions might be synchronized because of concurrent access to battle
     * instance from TurnControllerThread. TurnController and this method may change time of ending turn,
     * so we synchrinize it
     * @param player - battle player
     * @param act - action
     * @param args - action parameters
     */
    public abstract void executeSynchronizedAction(BattlePlayer player, BattlePlayerAction act, Object [] args);

    /**
     * This method is called after each ball put
     */
    protected void tryFinishBattle() {
        // TODO: implement algorithm of checking battle finished
    }

    public BallColor[] getColorsSetForRandomAccess() {
        if (colorsSetForRandomAccess == null) {
            colorsSetForRandomAccess = generateColorsSetForRandomAccess();
        }
        return colorsSetForRandomAccess;
    }

    private BallColor[] generateColorsSetForRandomAccess() {
        List<BallColor> result = new ArrayList<BallColor>();
        for (BattlePlayer player : battleMembers) {
            result.add(player.getBallColor());
        }
        for (BallColor color : BallColor.values()) {
            result.add(color);
        }
        return result.toArray(new BallColor[result.size()]);
    }
}
